Лабораторная работа 16 "Оценка производительности"

Допуск к лабораторной работе

Данная лабораторная работа будет полностью опираться на навыки, полученные в ходе выполнения лабораторных работ:

  1. Периферийные устройства
  2. Программирование
  3. Программатор

Цель

Дать количественную оценку, характеризующую производительность реализованной вычислительной системы. На текущий момент мы создали процессорную систему, которая способна взаимодействовать с внешним миром посредством периферийных устройств ввода-вывода и программатора, по сути являющуюся компьютером. Однако встает вопрос, какое место данная система занимает в ряду уже существующих вычислительных систем.

Для оценки производительности необходимо модифицировать существующую процессорную систему, а после собрать и запустить специализированное ПО, отвечающее за измерение производительности (будет использована программа Coremark).

Теория

Coremark (далее кормарк) — это набор синтетических тестов (специальных программ) для измерения производительности процессорной системы. В данный набор входят такие тесты, как работа со связными списками, матричные вычисления, обработка конечных автоматов и подсчет контрольной суммы. Результат выражается в одном числе, которое можно использовать для сравнения с результатами других процессорных систем.

Для подсчета производительности, кормарк опирается на функцию, возвращающую текущее время, поэтому для оценки производительности нам потребуется вспомогательное периферийное устройство: таймер.

Для вывода результатов тестирования, необходимо описать способ, которым кормарк сможет выводить очередной символ сообщения — для этого мы будем использовать контроллер UART из предыдущих лабораторных работ.

Кроме того, скомпилированная без оптимизаций программа будет занимать чуть более 32KiB, поэтому нам потребуется изменить размер памяти инструкций.

Таким образом, для того чтобы запустить данную программу, нам необходимо выполнить как аппаратные изменения процессорной системы (добавить таймер и (если отсутствует) контроллер UART), так и программные изменения самого кормарка (для этого в нем предусмотрены специальные платформозависимые файлы, в которых объявлены функции, реализацию которых нам необходимо выполнить).

Задание

  1. Реализовать модуль-контроллер "таймер".
  2. Подключить этот модуль к системной шине. 2.1. В случае, если до этого в ЛР13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине готовый модуль uart_tx_sb_ctrl.
  3. Добавить реализацию платформозависимых функций программы coremark.
  4. Скомпилировать программу.
  5. Изменить размер памяти инструкций.
  6. Запустить моделирование.
  7. Сравнить результаты измерения производительности с результатами существующих процессорных системам.

Таймер

Разберемся с тем, как будет работать наш таймер. По сути, это просто системный счетчик (не путайте с программным счетчиком), непрерывно считающий такты с момента последнего сброса. Системным он называется потому, что работает на системной тактовой частоте. Для измерения времени мы будем засекать значение счетчика на момент начала отсчета и значение счетчика в конце отсчета. Зная тактовую частоту и разность между значениями счетчика мы с легкостью сможем вычислить прошедшее время. При этом нужно обеспечить счетчик такой разрядностью, чтобы он точно не смог переполниться.

Поскольку мы уже назвали данный модуль "таймером", чтобы тот не был слишком простым, давайте добавим ему функциональности: пускай это будет устройство, способное генерировать прерывание через заданное число тактов. Таким образом, процессорная система сможет засекать время без постоянного опроса счетчика.

Было бы удобно, чтобы мы могли управлять тем, каким образом данный модуль будет генерировать такое прерывание: однократно, заданное число раз, или же бесконечно, пока тот не остановят.

Таким образом, мы сформировали следующее адресное пространство данного контроллера:

АдресРежим доступаДопустимые значенияФункциональное назначение
0x00R[0:2⁶⁴-1]Значение системного счетчика, доступное только для чтения
0x04RW[0:2⁶⁴-1]Указание задержки, спустя которую таймер будет генерировать прерывание
0x08RW[0:2]Указание режима генерации прерываний (выключен, заданное число раз, бесконечно)
0x0cRW[0:2³²-1]Указание количества повторений генерации прерываний
0x24W1Программный сброс

Прототип модуля следующий:

module timer_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic        clk_i,
  input  logic        rst_i,
  input  logic        req_i,
  input  logic        write_enable_i,
  input  logic [31:0] addr_i,
  input  logic [31:0] write_data_i,  // не используется, добавлен для
                                     // совместимости с системной шиной
  output logic [31:0] read_data_o,
  output logic        ready_o,
/*
    Часть интерфейса модуля, отвечающая за отправку запросов на прерывание
    процессорного ядра
*/
  output logic        interrupt_request_o
);

Для работы данного контроллера потребуются следующие сигналы:

logic [63:0] system_counter;
logic [63:0] delay;
enum logic [1:0] {OFF, NTIMES, FOREVER} mode, next_mode;
logic [31:0] repeat_counter;
logic [63:0] system_counter_at_start;
  • system_counter — регистр, ассоциированный с адресом 0x00, системный счетчик. Задача регистра заключается в ежетактном увеличении на единицу.
  • delay — регистр, ассоциированный с адресом 0x04. Число тактов, спустя которое таймер (когда тот будет включен) сгенерирует прерывание. Данный регистр изменяется только сбросом, либо запросом на запись.
  • mode — регистр, ассоциированный с адресом 0x08. Режим работы таймера:
    • OFF — отключен (не генерирует прерывания)
    • NTIMES — включен до тех пор, пока не сгенерирует N прерываний (Значение N хранится в регистре repeat_counter и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режим OFF.
    • FOREVER — бесконечная генерация прерываний. Не отключится, пока режим работы прерываний не будет изменен.
  • next_mode — комбинационный сигнал, который подается на вход записи в регистр mode (аналог next_state из предыдущей лабораторной работы).
  • repeat_counter — регистр, ассоциированный с адресом 0x0c. Количество повторений для режима NTIMES. Уменьшается в момент генерации прерывания в этом режиме.
  • system_counter_at_start — неархитектурный регистр, хранящий значение системного счетчика на момент начала отсчета таймера. Обновляется при генерации прерывания (если это не последнее прерывание в режиме NTIMES) и при запросе на запись в регистр mode значения не OFF.

Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР13: 0x08. Таким образом, для обращения к системному счетчику, процессор будет использовать адрес 0x08000000 для обращения к регистру delay 0x08000004 и т.п.

Настройка Coremark

В первую очередь, необходимо скачать исходный код данной программы, размещенный по адресу: https://github.com/eembc/coremark. На случай возможных несовместимых изменений в будущем, все дальнейшие ссылки будут даваться на слепок репозитория, который был на момент коммита d5fad6b.

После этого, чтобы добавить поддержку нашей процессорной системы потребуется:

  1. Реализовать функцию, измеряющую время
  2. Реализовать функцию, выводящую очередной символ сообщения с результатами
  3. Реализовать функцию, выполняющую первичную настройку периферии перед тестом
  4. Выполнить мелкую подстройку, такую как количество итераций в тесте и указание аргументов, с которыми будет скомпилирована программа.

Все файлы, содержимое которых мы будем менять расположены в папке barebones.

1. Реализация функции, измеряющей время

Не мы первые придумали измерять время путем отсчета системных тактов, поэтому вся логика по измерению времени уже реализована в coremark. От нас требуется только реализовать функцию, которая возвращает текущее значение системного счетчика.

Данной функцией является barebones_clock, расположенная в файле core_portme.c. В данный момент, в реализации функции описан вызов ошибки (поскольку реализации как таковой нет). Мы должны заменить реализацию функции следующим кодом:

barebones_clock()
{
    volatile ee_u32 *ptr = (ee_u32*)0x08000000;
    ee_u32 tim = *ptr;
    return tim;
}

После ЛР14 вы уже должны представлять, что здесь происходит. Мы создали указатель с абсолютным адресом 0x08000000 — адресом системного счетчика. Разыменование данного указателя вернет текущее значение системного счетчика, что и должно быть результатом вызова этой функции.

Для того, чтобы корректно преобразовать тики системного счетчика во время, используется функция time_in_secs, которая уже реализована, но для работы которой нужно определить макрос CLOCKS_PER_SEC, характеризующий тактовую частоту, на которой работает процессор. Давайте определим данный макрос сразу над макросом EE_TICKS_PER_SEC:

#define CLOCKS_PER_SEC             10000000

На этом наша задача по измерению времени завершена. Остальные правки будут не сложнее этих.

2. Реализация вывода очередного символа сообщения

Для вывода очередного символа во встраиваемых системах используется (какое совпадение!) функция uart_send_char, расположенная в файле ee_printf.c.

В реализации данной функции вам уже предлагают алгоритм, по которому та должна работать. Необходимо:

  1. дождаться готовности UART к отправке;
  2. передать отправляемый символ;
  3. дождаться готовности UART к отправке (завершения передачи).

Давайте так и реализуем эту функцию:

uart_send_char(char c)
{
    volatile ee_u8 *uart_ptr = (ee_u8 *)0x06000000;
    while(*(uart_ptr+0x08));
    *uart_ptr = c;
    while(*(uart_ptr+0x08));
}

0x06000000 — базовый адрес контроллера UART TX из ЛР13 (и адрес передаваемых этим контроллером данных). 0x08 — смещение до адреса регистра busy в адресном пространстве этого контроллера.

3. Реализация функции первичной настройки

Это функция portable_init, расположена в уже известном ранее файле [core_portme.c]. Данная функция выполняет необходимые нам настройки перед началом теста. Для нас главное — настроить нужным образом контроллер UART. Допустим, мы хотим чтобы данные передавались на скорости 115200, c одним стоповым битом и контролем бита четности. В этом случае, мы должны добавить в начало функции следующий код:

portable_init(core_portable *p, int *argc, char *argv[])
{
    volatile ee_u32 *uart_tx_ptr = (ee_u32 *)0x06000000;
    *(uart_tx_ptr + 3) = 115200;
    *(uart_tx_ptr + 4) = 1;
    *(uart_tx_ptr + 5) = 1;

    //...
}

4. Дополнительные настройки

Для тонких настроек используется заголовочный файл core_portme.h, куда также требуется внести несколько изменений. Нам необходимо:

  1. Объявить в начале файла макрос ITERATIONS, влияющий на количество прогонов теста. Нам достаточно выставить значение 1.
  2. Обновить значение макроса COMPILER_FLAGS, заменив его значение FLAGS_STR на"-march=rv32i_zicsr -mabi=ilp32", именно с этими аргументами мы будем собирать программу. Это опциональная настройка, которая позволит вывести флаги компиляции в итоговом сообщении.
  3. Добавить подключение заголовочного файла #include <stddef.h>.

Компиляция

Для компиляции программы, вам потребуются предоставленные файлы Makefile и linker_script.ld, а также файл startup.S из ЛР14. Эти файлы необходимо скопировать с заменой в корень папки с программой.

Makefile написан из расчёта, что кросс-компилятор расположен по пути C:/riscv_cc/. В случае, если это не так, измените первую строчку данного файла в соответствии с расположением кросс-компилятора.

Для запуска компиляции, необходимо выполнить следующую команду, находясь в корне программы coremark:

make

В случае, если на вашем рабочем компьютере не установлена утилита make, то вы можете скомпилировать программу вручную, выполнив следующую последовательность команд:

cp barebones/*.c barebones/*.h ./
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_main.c -o core_main.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" startup.S -o startup.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_list_join.c -o core_list_join.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_matrix.c -o core_matrix.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_portme.c -o core_portme.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_state.c -o core_state.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_util.c -o core_util.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" cvt.c -o cvt.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" ee_printf.c -o ee_printf.o
/c/riscv_cc/bin/riscv-none-elf-gcc core_main.o startup.o core_list_join.o core_matrix.o core_portme.o core_state.o core_util.o cvt.o ee_printf.o -Wl,--gc-sections -nostartfiles -T linker_script.ld -march=rv32i_zicsr -mabi=ilp32 -I"./" -o coremark.elf
/c/riscv_cc/bin/riscv-none-elf-objdump -D coremark.elf > coremark_disasm.S
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .data -j .sdata -j .bss coremark.elf coremark_data.mem
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .text coremark.elf coremark_instr.mem
/c/riscv_cc/bin/riscv-none-elf-size coremark.elf
sed -i '1d' coremark_data.mem

В случае успешной компиляции, вам будет выведено сообщение об итоговом размере секций инструкций и данных:

   text    data     bss     dec     hex filename
  34324    2268     100   36692    8f54 coremark.elf

Изменение размера памяти инструкций

Как видите, размер секции инструкций превышает 32KiB на 1556 байт (32768—34324). Поэтому на время оценки моделирования, нам придется увеличить размер памяти инструкций до 64KiB, изменив число слов памяти инструкций до 16384. При этом необходимо изменить диапазон бит адреса, используемых для чтения инструкции из памяти с [11:2] на [15:2].

Обратите внимание, что увеличение размера памяти в 16 раз приведет к значительному увеличению времени синтеза устройства, поэтому данное изменение мы производим исключительно на время поведенческого моделирования.

Запуск моделирования

Программирование 32KiB по UART займет ощутимое время, поэтому вам предлагается проинициализировать память инструкций и данных "по старинке" через системные функции $readmemh.

Если все было сделано без ошибок, то примерно на 276ms времени моделирования вам начнется выводиться сообщение вида:

CoreMark Size    : 666
Total ticks      : 2574834
Total time (secs): <скрыто то получения результатов моделирования>
Iterations/Sec   : <скрыто то получения результатов моделирования>
ERROR! Must execute for at least 10 secs for a valid result!
Iterations       : 1
Compiler version : GCC13.2.0
Compiler flags   : -march=rv32i_zicsr -mabi=ilp32
Memory location  : STACK
seedcrc          : 0x29f4
[0]crclist       : 0x7704
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0x7704
Correct operation validated. See README.md for run and reporting rules.

(вывод сообщения будет завершен приблизительно на 335ms времени моделирования).

Порядок выполнения задания

  1. Опишите таймер в виде модуля timer_sb_ctrl.
  2. Проверьте описанный модуль с помощью тестового окружения tb_timer.
  3. Подключите timer_sb_ctrl к системной шине. Сигнал прерывания этого модуля подключать не нужно. 2.1 В случае, если до этого в ЛР13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине и готовый модуль uart_tx_sb_ctrl.
  4. Получите исходники программы Coremark. Для этого можно либо склонировать репозиторий, либо скачать его в виде архива со страницы: https://github.com/eembc/coremark.
  5. Добавьте реализацию платформозависимых функций программы coremark. Для этого в папке barebones необходимо:
    1. в файле core_portme.c:
      1. реализовать функцию barebones_clock, возвращающую текущее значение системного счетчика;
      2. объявить макрос CLOCKS_PER_SEC, характеризующий тактовую частоту процессора;
      3. реализовать функцию portable_init, выполняющую первичную инициализацию периферийных устройств до начала теста;
    2. в файле ee_printf.c реализовать функцию uart_send_char, отвечающую за отправку очередного символа сообщения о результате.
  6. Добавьте с заменой в корень программы файлы Makefile, linker_script.ld и startup.S.
  7. Скомпилируйте программу вызовом make.
    1. Если кросскомпилятор расположен не в директории C:/riscv_cc, перед вызовом make вам необходимо соответствующим образом отредактировать первую строчку в Makefile.
    2. В случае отсутствия на компьютере утилиты make, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в разделе "Компиляция".
  8. Временно измените размер памяти инструкций до 64KiB.
    1. Для этого необходимо изменить размер памяти инструкций с 1024 слов до 16384 слов.
    2. Кроме того, необходимо изменить используемые индексы адреса в памяти с [11:2] на [15:2].
  9. Проинициализируйте память инструкций и память данных файлами "coremark_instr.mem" и "coremark_data.mem", полученными в ходе компиляции программы.
  10. Выполните моделирование системы с помощью модуля tb_coremark.
    1. Результаты теста будут выведены приблизительно на 335ms времени моделирования.
11. Прочти меня после успешного завершения моделирования

Итак, вы получили сообщение вида:

CoreMark Size    : 666
Total ticks      : 2574834
Total time (secs): 0.257483
Iterations/Sec   : 3.883746
ERROR! Must execute for at least 10 secs for a valid result!
Iterations       : 1
Compiler version : GCC13.2.0
Compiler flags   : -march=rv32i_zicsr -mabi=ilp32
Memory location  : STACK
seedcrc          : 0x29f4
[0]crclist       : 0x7704
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0x7704
Correct operation validated. See README.md for run and reporting rules.

Не обращайте внимание на строчку "ERROR! Must execute for at least 10 secs for a valid result!". Программа считает, что для корректных результатов, необходимо крутить ее по кругу в течении минимум 10 секунд, однако по большей части это требование необходимо для более достоверного результата у систем с кэшем/предсказателями переходов и прочими блоками, которые могут изменить количество тактов на прохождение между итерациями. Наш однотактный процессор будет вести себя одинаково на каждом круге, поэтому нет смысла в дополнительном времени моделирования.

Нас интересует строка:

Iterations/Sec   : 3.883746

Это и есть так называемый "кормарк" — метрика данной программы. Результат нашего процессора: 3.88 кормарка.

Обычно, для сравнения между собой нескольких реализаций микроархитектур, более достоверной считается величина "кормарк / МГц", т.е. значение кормарка, поделённое на тактовую частоту процессора. Дело в том, что можно реализовать какую-нибудь очень сложную архитектуру, которая будет выдавать очень хороший кормарк, но при этом будет иметь очень низкую частоту. Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат.

Мы не будем уходить в дебри темных паттернов маркетинга и вместо этого будет оценивать производительность в лоб: сколько кормарков в секунду смог прогнать наш процессор в сравнении с представленными результатами других систем вне зависимости от их оптимизаций.

Таблица опубликованных результатов находится по адресу: https://www.eembc.org/coremark/scores.php. Нам необходимо отсортировать эту таблицу по столбцу CoreMark, кликнув по нему.

Мы получим следующий расклад:

../../.pic/Labs/lab_16_coremark/fig_01.png

На что мы можем обратить внимание? Ну, во-первых, мы видим, что ближайший к нам микроконтроллер по кормарку — это ATmega2560 с результатом 4.25 кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino.

Есть ли здесь еще что-нибудь интересное? Посмотрим в верх таблицы, мы можем увидеть производителя Intel с их микропроцессором Intel 80286. Как написано на вики, данный микропроцессор был в 3-6 раз производительней Intel 8086, который соперничал по производительности с процессором Zilog Z80, который устанавливался в домашний компьютер TRS-80.

А знаете, с чем был сопоставим по производительности компьютер TRS-80? С бортовым компьютером Apollo Guidance Computer, который проводил вычисления и контролировал движение, навигацию, управлял командным и лунным модулями в ходе полётов по программе Аполлон.

Иными словами, мы разработали процессор, который приблизительно в 7-14 раз производительнее компьютера, управлявшего полетом космического корабля, который доставил человека на Луну!

Можно ли как-то улучшить наш результат? Безусловно. Мы можем улучшить его примерно на 5% изменив буквально одну строчку. Дело в том, что для простоты реализации, мы генерировали сигнал stall для каждой операции обращения в память. Однако приостанавливать работу процессора было необходимо только для операций чтения из памяти. Если не генерировать сигнал stall для операций типа store, мы уменьшим время, необходимое на исполнение бенчмарка. Попробуйте сделать это сами.

Добавление умножителей, конвейеризация и множество других потенциальных улучшений увеличат производительность в разы.

Но это, как говорится, уже другая история.